package com.g.ocache.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.LinkedList;
import java.util.List;

/**
 * 
 * @author Jingqi Xu
 * Exponent moving average algorithm
 */
public final class EMA<T extends Number> {
	//
	private static final BigDecimal BD_2 = new BigDecimal("2");
	
	//
	private final int scale;
	private final int period;
	private final BigDecimal periodPlus1;
	private final BigDecimal periodMinus1;
	
	//
	private T min;
	private T max;
	private double avg;
	private boolean dirty;
	private final List<T> samples;
	
	/**
	 * 
	 */
	public EMA(int period) {
		this(period, 0);
	}
	
	public EMA(int period, int scale) {
		//
		this.period = period;
		this.scale = scale;
		this.periodPlus1 = new BigDecimal(period + 1);
		this.periodMinus1 = new BigDecimal(period - 1);
		
		//
		this.dirty = true;
		this.samples = new LinkedList<T>();
	}

	/**
	 * 
	 */
	public int getPeriod() {
		return period;
	}
	
	public int getScale() {
		return scale;
	}
	
	/**
	 * 
	 */
	public synchronized void reset() {
		this.samples.clear();
		this.dirty = true;
	}
	
	public synchronized T getMin() {
		//
		if(dirty) {
			calculate();
		}
		
		//
		return min;
	}
	
	public synchronized T getMax() {
		//
		if(dirty) {
			calculate();
		}
		
		//
		return max;
	}
	
	public synchronized double getAverage() {
		//
		if(dirty) {
			calculate();
		}
		
		//
		return avg;
	}
	
	public synchronized void sample(T t) {
		//
		if(t == null) {
			return;
		}
		
		//
		if(samples.size() >= (period + 1)) {
			samples.remove(0);
		}
		samples.add(t);
		
		//
		dirty = true;
	}
	
	/**
	 * 
	 */
	private void calculate() {
		//
		if(samples.size() == 0) {
			min = null;
			max = null;
			avg = 0.0d;
			dirty = false;
			return;
		}
		
		//
		T first = samples.get(0);
		this.min = first;
		this.max = first;
		BigDecimal ema = BigDecimal.valueOf(first.doubleValue());
		for(int i = 1; i < samples.size(); i++) {
			// min & max
			T t = samples.get(i);
			if(t.doubleValue() < min.doubleValue()) {
				min = t;
			}
			if(t.doubleValue() > max.doubleValue()) {
				max = t;
			}
			
			// avg
			ema = ema.multiply(periodMinus1);
			ema = ema.add(BigDecimal.valueOf(t.doubleValue()).multiply(BD_2));
			ema = ema.divide(periodPlus1, scale, RoundingMode.HALF_UP);
		}
		this.avg = ema.doubleValue();
		
		//
		dirty = false;
	}
}
